// TODO: CODE REVIEW & CLEANUP!
//
// JdsSimulator.cs
//
// Implements HomeUX.Components.JdsSimulator.
//

using System;
using System.Globalization;
using System.IO.Ports;
using System.Text;
using System.Text.RegularExpressions;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Simulates a HomeUX driver for the JDS Technologies IR-XP2 "InfraRed Xpander", which transmits
/// IR codes.
/// </summary>
///
/// <remarks>
/// <para>
/// This is a limited simulation.  Only these commands are supported:
/// <list type="table">
///     <listheader>
///         <term>Command</term>
///         <description>Description</description>
///     </listheader>
///     <item>
///         <term><b>6C82C0CC6D</b></term>
///         <description>Resets the hardware.</description>
///     </item>
///     <item>
///         <term><b>6C85<i>mmaappee</i>iCC</b></term>
///         <description>Plays an IR sample.  <i>mm</i> = F5 (lower memory), F7 (upper memory);
///				<i>aa</i> = sample address (00=0 to F9=249); <i>pp</i> = number of times to play
/// 			the sample (00=0 to FF=255); <i>ee</i> = emitter ports (00=none, 01=port 1, ...
/// 			1F=ports 1 through 5).</description>
///     </item>
/// </list>
/// </para>
/// The following constructor arguments are supported (see <r>Util.CreateInstance</r>):
/// <list type="table">
///     <listheader>
///         <term>Argument</term>
///         <description>Description</description>
///     </listheader>
///     <item>
///         <term><b>Test</b></term>
///         <description>(For development use only.)  The ID of the test to execute.  The default
///             is 0, indicating no test.</description>
///     </item>
/// </list>
/// </remarks>
///
public class JdsSimulator : VirtualSerialPort
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The JDS command to reset the hardware.
    /// </summary>
    const string ResetCommand = "6C82C0CC6D";

    /// <summary>
    /// Matches the JDS command to play an IR sample.
    /// </summary>
    static readonly Regex PlayCommandRegex = new Regex(@"^6C85(..)(..)(..)(..)CC");

    ///////////////////////////////////////////////////////////////////////////////////////////////
	// Private State
	//

	/// <summary>
	/// This object is locked before a thread accesses this object, to serialize access to the
	/// object.
	/// </summary>
	object _lock = new object();

    /// <summary>
    /// Input received from the application.  Once input has been processed, it is removed from
    /// this buffer.
    /// </summary>
    string _received;

	/// <summary>
	/// Serial port read buffer, i.e. data to send to client.
	/// </summary>
	StringBuilder _sendToClient = new StringBuilder(1000);

	/// <summary>
	/// Work queue for scheduling asynchronous operations.
	/// </summary>
	WorkQueue _workQueue;

    /// <summary>
    /// Identifies a test being performed using this simulator, or 0 if no test is being
    /// performed.
    /// </summary>
    int _test;

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Public Methods
	//

	/// <summary>
	/// Creates an instance of this class.
	/// </summary>
	///
	public JdsSimulator() : this(new string[0])
	{
	}

    public JdsSimulator(string[] args)
    {
        // parse <args>
        foreach (string arg in args)
        {
            try
            {
                string[] argParts = arg.Split('=');
                if (argParts.Length != 2)
                    throw new ArgumentException();
                string argName = argParts[0];
                string argValue = argParts[1];
                switch (argName)
                {
                case "Test":
                    _test = System.Xml.XmlConvert.ToInt32(argValue);
                    break;
                default:
                    throw new ArgumentException();
                }
            }
            catch (Exception ex)
            {
                if ((ex is FormatException) || (ex is OverflowException) ||
                    (ex is ArgumentException))
                {
                    throw new VirtualSerialPortException("Invalid argument: {0}", arg);
                }
                else
                    throw;
            }
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public override void Dispose()
    {
		lock (_lock)
		{
			base.Dispose();
			if (_workQueue != null)
			{
				_workQueue.Dispose();
				_workQueue = null;
			}
		}
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// VirtualSerialPort Overrides
	//

    public override bool SupportsCharacterBasedTextMode
    {
        // this virtual serial port only supports character-based text mode
        get
        {
            return true;
        }
    }

    public override void Open()
    {
        lock (_lock)
        {
			// create a work queue for queueing events
			_workQueue = new WorkQueue("JdsSimulator");

			// debugging feedback
			Trace("Initialized");

            if (_test == 1)
                _sendToClient.Append("RandomJunk\r\nFromJdsSimulator");
		}
    }

	public override string ReadExisting()
	{
		lock (_lock)
		{
			string text = _sendToClient.ToString();
			_sendToClient.Length = 0;
			return text;
		}
	}

    public override void Write(string text)
    {
        _received += text;

        while (_received.Length > 0)
        {
            Match match;
            if (_received.StartsWith(ResetCommand))
            {
                // we found the Reset command
                Trace("Reset");
                WriteToClient("6CC0 A A I R X P 2 E0000FF");
                if (_test == 1)
                    _sendToClient.Append("MoreRandomJunkFromJdsSimulator");
                _received = _received.Substring(ResetCommand.Length);
            }
            else
            if ((match = PlayCommandRegex.Match(_received)).Success)
            {
                // we found the Play command
                try
                {
                    string memoryString = match.Groups[1].Value;
                    string addressString = match.Groups[2].Value;
                    string playCountString = match.Groups[3].Value;
                    string emitterPortsString = match.Groups[4].Value;
                    _received = _received.Substring(match.Length);

                    bool upperMemory;
                    if (memoryString == "F5")
                        upperMemory = false;
                    else
                    if (memoryString == "F7")
                        upperMemory = true;
                    else
                        throw new SimulatorException("Expecting F5 or F7, got: {0}", memoryString);

                    int address = ParseHex(addressString, 0, 250);
                    int playCount = ParseHex(playCountString, 0, 255);
                    int emitterPorts = ParseHex(emitterPortsString, 0, 255) & 0x1F;

                    var portsList = new StringBuilder();
                    for (int emitterPort = 1; emitterPort <= 6; emitterPort++)
                    {
                        if ((emitterPorts & 1) == 1)
                        {
                            if (portsList.Length > 0)
                                portsList.Append(", ");
                            portsList.AppendFormat("{0}", emitterPort);
                        }
                        emitterPorts = emitterPorts >> 1;
                    }
                    if (portsList.Length == 0)
                        portsList.Append("(none)");

                    Trace("Play IR sample {0}, from {1} memory, {2} time(s), to emitter port(s) {3}",
                        address, (upperMemory ? "upper" : "lower"), playCount, portsList);
                }
                catch (SimulatorException ex)
                {
                    Trace("Play command failed: {0}", ex.Message);
                }
            }
            else
            if (_received.Length >= 50/*longest command*/)
            {
                Trace("Ignoring input character: {0}", _received[0]);
                _received = _received.Substring(1);
            }
            else
                break;
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Private Properties & Methods
	//

	// throws SimulatorException
    int ParseHex(string stringValue, int minValue, int maxValue)
    {
		int intValue;
        if (!int.TryParse(stringValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out intValue))
            throw new SimulatorException("Not a valid hexadecimal number: {0}", stringValue);
        if ((intValue < minValue) || (intValue > maxValue))
            throw new SimulatorException("Hexadecimal value \"{0}\" (decimal {1}) is out of range ({2} to {3}, decimal)",
                stringValue, intValue, minValue, maxValue);
        return intValue;
	}

	/// <summary>
	/// Formats a string and sends it string to the client asynchronously.  The client will receive
	/// the string on the virtual serial port.
	/// </summary>
	/// 
	/// <param name="format">The error message format string.  If <pr>args</pr> is empty,
	/// 	<pr>format</pr> is used without formatting.</param>
	///
	/// <param name="args">Formatting arguments.</param>
	///
	void WriteToClient(string format, params object[] args)
	{
		lock (_lock)
		{
			WriteToClient((args.Length == 0) ? format : String.Format(format, args));
		}
	}

	/// <summary>
	/// Sends a string to the client asynchronously.  The client will receive the string on the
	/// virtual serial port.
	/// </summary>
	///
	void WriteToClient(string text)
	{
		lock (_lock)
			_sendToClient.Append(text);
		_workQueue.Add(delegate
		{
            FireDataReceived(SerialData.Chars);
		});
	}

    /// <summary>
    /// Writes a formatted string to the debug output window.
    /// </summary>
    ///
    static void Trace(string format, params object[] args)
    {
        // static -- no lock required
        Util.Trace("JdsSimulator: {0}", Util.Format(format, args));
    }

	// nested types

    class SimulatorException : Exception
    {
        internal SimulatorException(string format, params object[] args) :
            base(Util.Format(format, args))
	    {
	    }
    }
}

}

